package cn.zyl.demo.common.gif;

import java.io.*;
import java.awt.*;
import java.awt.image.*;

/**
 * 类AnimatedGifEncoder-对由一个或多个帧组成的GIF文件进行编码。
 * <pre>
 * 例子:
 *    AnimatedGifEncoder e = new AnimatedGifEncoder();
 *    e.start(outputFileName);
 *    e.setDelay(1000);   // 每秒1帧
 *    e.addFrame(image1);
 *    e.addFrame(image2);
 *    e.finish();
 * </pre>
 * 此类的源没有声明版权。可以使用.但是，出于任何目的，请参考 LZW 专利的限制
 * 使用关联的EncoderLZW类。请转发任何更正到kweiner@fmsware.com。
 *
 * @author Kevin Weiner, FM Software,wsz
 * @version 1.03 November 2003
 * @date 2020-08-19 <a href="https://www.xuebuyuan.com/3199369.html">源码</a>
 *
 */
public class AnimatedGifEncoder {
    private static final int DEF_WIDTH = 320;
    private static final int DEF_HEIGHT = 240;
    private static final float F_10 = 10.0f;
    private static final float F_0 = 0.0f;
    private static final float F_100 = 100.0f;
    /** 图片大小*/
    protected int width;
    protected int height;
    /** 如果给定，则为透明色*/
    protected Color transparent = null;
    /** 颜色表中的透明索引*/
    protected int transIndex;
    /** 不再重复*/
    protected int repeat = -1;
    /** 帧延迟（百分之一）*/
    protected int delay = 0;
    /** 准备输出帧*/
    protected boolean started = false;
    protected OutputStream out;
    /** 当前帧*/
    protected BufferedImage image;
    /** 帧中的BGR字节数组*/
    protected byte[] pixels;
    /** 转换为索引到调色板的框架*/
    protected byte[] indexedPixels;
    /** 位平面数*/
    protected int colorDepth;
    /** RGB调色板*/
    protected byte[] colorTab;
    /** 活动调色板条目*/
    protected boolean[] usedEntry = new boolean[256];
    /** 色表大小（位1）*/
    protected int palSize = 7;
    /** 处置代码（-1 =使用默认值）*/
    protected int dispose = -1;
    /** 完成后关闭流*/
    protected boolean closeStream = false;
    /** 是否第一帧*/
    protected boolean firstFrame = true;
    /** 如果为假，则从第一帧获取大小*/
    protected boolean sizeSet = false;
    /** 量化器的默认采样间隔*/
    protected int sample = 10;

    /**
     * 设置每个帧之间的延迟时间，或更改后续帧的延迟时间（适用于最后添加的帧）。
     *
     * @param ms int延迟时间（毫秒）
     */
    public void setDelay(int ms) {
        delay = Math.round(ms / F_10);
    }
    /**
     * 为最后添加的帧和任何后续帧设置GIF帧处理代码。如果未设置透明颜色，则默认值为0，否则为2。
     * @param code int处理代码。
     */
    public void setDispose(int code) { if (code >= 0) { dispose = code; } }
    /**
     * 设置一组GIF帧的播放次数。默认值为1；0表示无限期播放。必须在添加第一个映像之前调用。
     * @param iterations int迭代次数。
     */
    public void setRepeat(int iterations) { if (iterations >= 0) { repeat = iterations; } }
    /**
     * 为最后添加的帧和任何后续帧设置透明颜色。
     * 由于所有的颜色都会在量化过程中进行修改，因此最终调色板中每帧最接近给定颜色的颜色
     * 成为该帧的透明颜色。可以设置为null以指示没有透明颜色。
     * @param c 显示时应视为透明的颜色。
     */
    public void setTransparent(Color c) {
        transparent = c;
    }

    /**
     * 添加下一个GIF帧。
     * 帧不是立即写入的，而是实际上被延迟到下一帧被接收，以便可以插入定时数据。调用finish（）将刷新所有帧。如果未调用setSize，则第一个图像的大小将用于所有后续帧。
     * @param im 包含要写入的帧的BufferedImage
     * @return 如果成功，则为真。
     */
    public boolean addFrame(BufferedImage im) {
        if (im == null || !started) { return false; }
        boolean ok = true;
        try {
            if (!sizeSet) {
                // 使用第一帧的大小
                setSize(im.getWidth(), im.getHeight());
            }
            image = im;
            // 必要时转换为正确格式
            getImagePixels();
            // 构建颜色表和映射像素
            analyzePixels();
            if (firstFrame) {
                // 逻辑屏幕描述符
                writeLSD();
                // 全局颜色表
                writePalette();
                if (repeat >= 0) {
                    // 使用NS应用程序扩展来指示代表
                    writeNetscapeExt();
                }
            }
            // 写图形控制扩展
            writeGraphicCtrlExt();
            // 图像描述符
            writeImageDesc();
            // 局部颜色表
            if (!firstFrame) { writePalette(); }
            // 编码和写入像素数据
            writePixels();
            firstFrame = false;
        } catch (IOException e) {
            ok = false;
        }

        return ok;
    }
    /**
     * 刷新所有挂起的数据并关闭输出文件。如果写入OutputStream，则该流不会关闭。
     */
    public boolean finish() {
        if (!started) { return false; }
        boolean ok = true;
        started = false;
        try {
            // gif预告片
            out.write(0x3b);
            out.flush();
            if (closeStream) {
                out.close();
            }
        } catch (IOException e) {
            ok = false;
        }
        // 重置以备后续使用
        transIndex = 0;
        out = null;
        image = null;
        pixels = null;
        indexedPixels = null;
        colorTab = null;
        closeStream = false;
        firstFrame = true;
        return ok;
    }
    /**
     * 以每秒帧数为单位设置帧速率。相当于 <code>setDelay(1000/fps)</code>.
     * @param fps 浮动帧速率（每秒帧数）
     */
    public void setFrameRate(float fps) {
        if (fps != F_0) { delay = Math.round(F_100 / fps); }
    }
    /**
     * 颜色量化的套质量（图像的变换由GIF规范允许的最大256种颜色）。 较低的值（最小= 1）产生更好的颜色，但缓慢处理显著。
     * 10是默认的，并以合理的速度下产生良好的颜色映射。 值大于20，不要在速度产生显著的改善
     * @param quality int大于0。
     */
    public void setQuality(int quality) {
        if (quality < 1) { quality = 1; }
        sample = quality;
    }
    /**
     * 设置GIF帧大小。 默认大小为如果不被调用此方法加入的第一帧的大小
     * @param w int 帧 宽.
     * @param h int 帧 高.
     */
    public void setSize(int w, int h) {
        if (started && !firstFrame) { return; }
        width = w;
        height = h;
        if (width < 1) { width = DEF_WIDTH; }
        if (height < 1) { height = DEF_HEIGHT; }
        sizeSet = true;
    }
    /**
     * 在给定的流上启动GIF文件创建。流不会自动关闭。
     * @param os 写入GIF图像的OutputStream。
     * @return 如果初始写入失败，则返回false。
     */
    public boolean start(OutputStream os) {
        if (os == null) { return false; }
        started = true;
        closeStream = false;
        out = os;
        try {
            // 头部
            writeString("GIF89a");
        } catch (IOException e) {
            started = false;
        }
        return started;
    }
    /**
     * 启动与指定名称的GIF文件的写入
     * @param file 包含输出文件名的字符串。
     * @return 如果打开或初始写入失败，则为false。
     */
    public boolean start(String file) {
        try {
            out = new BufferedOutputStream(new FileOutputStream(file));
            started = start(out);
            closeStream = true;
        } catch (IOException e) {
            started = false;
        }
        return started;
    }
    /**
     * 分析图像的色彩和创建彩色地图
     */
    protected void analyzePixels() {
        int len = pixels.length;
        int nPix = len / 3;
        indexedPixels = new byte[nPix];
        NeuQuant nq = new NeuQuant(pixels, len, sample);
        // 初始化量化器，创建精简调色板
        colorTab = nq.process();
        // 将映射从BGR转换为RGB
        for (int i = 0; i < colorTab.length; i += 3) {
            byte temp = colorTab[i];
            colorTab[i] = colorTab[i + 2];
            colorTab[i + 2] = temp;
            usedEntry[i / 3] = false;
        }
        // 将图像像素映射到新调色板
        int k = 0;
        for (int i = 0; i < nPix; i++) {
            int index = nq.map(pixels[k++] & 0xff, pixels[k++] & 0xff, pixels[k++] & 0xff);
            usedEntry[index] = true;
            indexedPixels[i] = (byte) index;
        }
        pixels = null;
        colorDepth = 8;
        palSize = 7;
        // 如果指定，则获取与透明颜色最接近的匹配
        if (transparent != null) {
            transIndex = findClosest(transparent);
        }
    }
    /**
     * 返回最接近颜色的调色板颜色的索引
     */
    protected int findClosest(Color c) {
        if (colorTab == null) { return -1; }
        int r = c.getRed();
        int g = c.getGreen();
        int b = c.getBlue();
        int minpos = 0;
        int dmin = 256 * 256 * 256;
        int len = colorTab.length;
        for (int i = 0; i < len;) {
            int dr = r - (colorTab[i++] & 0xff);
            int dg = g - (colorTab[i++] & 0xff);
            int db = b - (colorTab[i] & 0xff);
            int d = dr * dr + dg * dg + db * db;
            int index = i / 3;
            if (usedEntry[index] && (d < dmin)) {
                dmin = d;
                minpos = index;
            }
            i++;
        }
        return minpos;
    }
    /**
     * 将图像像素提取到字节数组“pixels”
     */
    protected void getImagePixels() {
        int w = image.getWidth();
        int h = image.getHeight();
        int type = image.getType();
        if ((w != width) || (h != height) || (type != BufferedImage.TYPE_3BYTE_BGR)) {
            // 创建大小/格式正确的新图像
            BufferedImage temp = new BufferedImage(width, height, BufferedImage.TYPE_3BYTE_BGR);
            Graphics2D g = temp.createGraphics();
            g.drawImage(image, 0, 0, null);
            image = temp;
        }
        pixels = ((DataBufferByte) image.getRaster().getDataBuffer()).getData();
    }
    /**
     * 写入图形控制扩展
     */
    protected void writeGraphicCtrlExt() throws IOException {
        // 可拓导引器
        out.write(0x21);
        // GCE标签
        out.write(0xf9);
        // 数据块大小
        out.write(4);
        int transp, disp;
        if (transparent == null) {
            transp = 0;
            // dispose=无操作
            disp = 0;
        } else {
            transp = 1;
            // 如果使用透明颜色，则强制清除
            disp = 2;
        }
        if (dispose >= 0) {
            // 用户覆盖
            disp = dispose & 7;
        }
        disp <<= 2;

        // 压缩字段
        // 1:3 保留
        // 4:6 清除
        // 7   用户输入 - 0 = none
        // 8   透明度标志
        out.write(0 | disp | 0 | transp);
        // 延迟x 1/100秒
        writeShort(delay);
        // 透明色指数
        out.write(transIndex);
        // 块终止符
        out.write(0);
    }
    /**
     * 写入图像描述
     */
    protected void writeImageDesc() throws IOException {
        // 图像分隔符
        out.write(0x2c);
        // image 坐标 x,y = 0,0
        writeShort(0);
        writeShort(0);
        // image 大小
        writeShort(width);
        writeShort(height);
        // 压缩字段
        if (firstFrame) {
            // 无LCT-GCT用于第一帧（或唯一帧）
            out.write(0);
        } else {
            // 指定正常LCT
            // 1 局部颜色表  1=yes
            // 2 交叉 - 0=no
            // 3 排序 - 0=no
            // 4-5 保留
            // 6-8 颜色表大小
            out.write(0x80 | 0 | 0 | 0 | palSize);
        }
    }
    /**写逻辑屏幕描述*/
    protected void writeLSD() throws IOException {
        // 逻辑屏幕大小
        writeShort(width);
        writeShort(height);
        // 压缩字段
        // 1   : 全局颜色表标志 = 1 (使用 gct)
        // 2-4 : 颜色分辨率 = 7
        // 5   : gct 排序标志 = 0
        // 6-8 : gct 尺寸
        out.write((0x80 | 0x70 | 0x00 | palSize));
        // 背景色索引
        out.write(0);
        // 像素纵横比-假设为1:1
        out.write(0);
    }
    /** Netscape写入应用程序扩展定义重复计数*/
    protected void writeNetscapeExt() throws IOException {
        // 可拓导引器
        out.write(0x21);
        // 应用程序扩展标签
        out.write(0xff);
        // 块大小
        out.write(11);
        // 应用程序id+身份验证码
        writeString("NETSCAPE" + "2.0");
        // 子块大小
        out.write(3);
        // 循环子块id
        out.write(1);
        // 循环计数（额外迭代，0=永远重复）
        writeShort(repeat);
        // 块终止符
        out.write(0);
    }
    /**写入色表*/
    protected void writePalette() throws IOException {
        out.write(colorTab, 0, colorTab.length);
        int n = (3 * 256) - colorTab.length;
        for (int i = 0; i < n; i++) {
            out.write(0);
        }
    }
    /**编码和写入像素数据*/
    protected void writePixels() throws IOException {
        EncoderLZW encoder = new EncoderLZW(width, height, indexedPixels, colorDepth);
        encoder.encode(out);
    }
    /**写的16位值，以输出流，LSB第一*/
    protected void writeShort(int value) throws IOException {
        out.write(value & 0xff);
        out.write((value >> 8) & 0xff);
    }
    /**写入字符串输出流*/
    protected void writeString(String s) throws IOException {
        for (int i = 0; i < s.length(); i++) {
            out.write((byte) s.charAt(i));
        }
    }

    public static void main(String[] args) {
        //生成一个200x200，透明背景，黑色的圆循环向下运动的GIF动画
        int size = 200;
        AnimatedGifEncoder encoder = new AnimatedGifEncoder();
        encoder.start("out.gif");
        encoder.setTransparent(Color.WHITE);
        encoder.setRepeat(0);
        encoder.setDelay(30);

        BufferedImage img = new BufferedImage(size, size, BufferedImage.TYPE_3BYTE_BGR);
        Graphics2D g2d = img.createGraphics();

        for (int i=0; i<100; i++) {
            g2d.setColor(Color.WHITE);
            g2d.fillRect(0, 0, size, size);
            g2d.setColor(Color.BLACK);
            g2d.drawOval(0, i, 100, 100);

            encoder.addFrame(img);
        }

        g2d.dispose();
        encoder.finish();
    }
}
